﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="BooleanSettingDescriptor.cs" company="Radical Research Ltd">
//   Copyright (c) Radical Research Ltd. The MIT License (MIT)
// </copyright>
// <summary>
//   
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace RadicalResearch.MiniState.Descriptors
{
    using System;

    /// <summary>
    /// Manages Boolean settings stored in a value collection.
    /// </summary>
    public class BooleanSettingDescriptor : NumericSettingDescriptor
    {
        #region Constants and Fields

        /// <summary>
        ///   ValueKey used if non is specified.
        /// </summary>
        private const string DefaultValueKey = "0";

        /// <summary>
        ///   Index of the bit in the bitmask
        /// </summary>
        private readonly int bitIndex;

        /// <summary>
        ///   Type used to store bitmask
        /// </summary>
        private readonly Type type;

        #endregion

        #region Constructors and Destructors

        /// <summary>Initializes a new instance of the <see cref="BooleanSettingDescriptor"/> class.</summary>
        /// <param name="valueKey">The value key.</param>
        public BooleanSettingDescriptor(string valueKey)
            : this(valueKey, 0, BitValueSize.SingleBit)
        {
        }

        /// <summary>Initializes a new instance of the <see cref="BooleanSettingDescriptor"/> class.</summary>
        /// <param name="bitIndex">Index of the bit.</param>
        public BooleanSettingDescriptor(int bitIndex)
            : this(DefaultValueKey, bitIndex, BitValueSize.EightBits)
        {
        }

        /// <summary>Initializes a new instance of the <see cref="BooleanSettingDescriptor"/> class.</summary>
        /// <param name="valueKey">The value key.</param>
        /// <param name="bitIndex">Index of the bit.</param>
        public BooleanSettingDescriptor(string valueKey, int bitIndex)
            : this(valueKey, bitIndex, BitValueSize.EightBits)
        {
        }

        /// <summary>Initializes a new instance of the <see cref="BooleanSettingDescriptor"/> class.</summary>
        /// <param name="valueKey">The value key.</param>
        /// <param name="bitIndex">Index of the bit.</param>
        /// <param name="size">The number of bits used to store the value.</param>
        public BooleanSettingDescriptor(string valueKey, int bitIndex, BitValueSize size)
            : base(valueKey, false, TypeFromSize(size))
        {
            if ((int)size <= bitIndex)
            {
                throw new ArgumentOutOfRangeException("bitIndex", "bit index must be less than size");
            }

            this.type = TypeFromSize(size);
            this.bitIndex = bitIndex;
        }

        #endregion

        #region Enums

        /// <summary>Possible bitmask sizes</summary>
        public enum BitValueSize
        {
            /// <summary>
            ///   Single bit
            /// </summary>
            SingleBit = 1, 

            /// <summary>
            ///   Eight bits
            /// </summary>
            EightBits = 8, 

            /// <summary>
            ///   Sixteen bits
            /// </summary>
            SixteenBits = 16, 

            /// <summary>
            ///   Thirty two bits
            /// </summary>
            ThirtyTwoBits = 32, 

            /// <summary>
            ///   Sixty four bits
            /// </summary>
            SixtyFourBits = 64
        }

        #endregion

        #region Properties

        /// <summary>
        ///   Gets the index of the bit within the bitmask
        /// </summary>
        public int BitIndex
        {
            get
            {
                return this.bitIndex;
            }
        }

        #endregion

        #region Public Methods

        /// <summary>Reads the value.</summary>
        /// <param name="values">The values.</param>
        /// <returns>the value as an object</returns>
        public override object ReadValue(IValueCollection values)
        {
            bool value = false;
            object obj = base.ReadValue(values);
            if (null != obj)
            {
                ulong bits = Convert.ToUInt64(obj);
                value = GetBitValue(bits, this.bitIndex);
            }

            return value;
        }

        /// <summary>Writes the value.</summary>
        /// <param name="values">The values.</param>
        /// <param name="value">The value.</param>
        public override void WriteValue(IValueCollection values, object value)
        {
            ulong bits = 0;
            object obj = base.ReadValue(values);
            if (null != obj)
            {
                bits = Convert.ToUInt64(obj);
            }

            bits = SetBitValue(bits, this.bitIndex, (bool)value);
            obj = Convert.ChangeType(bits, this.type);
            base.WriteValue(values, obj);
        }

        #endregion

        #region Methods

        /// <summary>Gets the Boolean value of the bit at the specified index.</summary>
        /// <param name="bits">The value containing the bits.</param>
        /// <param name="bitIndex">Index of the bit.</param>
        /// <returns>Boolean value of the bit at the specified index</returns>
        private static bool GetBitValue(ulong bits, int bitIndex)
        {
            ulong mask = (ulong)(1 << bitIndex);
            return (bits & mask) == mask;
        }

        /// <summary>Sets the Boolean value of the bit at the specified index.</summary>
        /// <param name="bits">The value containing the bits.</param>
        /// <param name="bitIndex">Index of the bit.</param>
        /// <param name="value">if set to <c>true</c> set the bit on, otherwise set the bit off.</param>
        /// <returns>updated value after the bit has been changed</returns>
        private static ulong SetBitValue(ulong bits, int bitIndex, bool value)
        {
            ulong mask = (ulong)(1 << bitIndex);
            if (value)
            {
                bits |= mask;
            }
            else
            {
                bits &= ~mask;
            }

            return bits;
        }

        /// <summary>Gets a type from the specified bit size</summary>
        /// <param name="size">number of bits</param>
        /// <returns>value type with the specified number of bits</returns>
        /// <exception cref="ArgumentOutOfRangeException"></exception>
        private static Type TypeFromSize(BitValueSize size)
        {
            switch (size)
            {
                case BitValueSize.SingleBit:
                    return typeof(bool);
                case BitValueSize.EightBits:
                    return typeof(byte);
                case BitValueSize.SixteenBits:
                    return typeof(ushort);
                case BitValueSize.ThirtyTwoBits:
                    return typeof(uint);
                case BitValueSize.SixtyFourBits:
                    return typeof(ulong);
                default:
                    throw new ArgumentOutOfRangeException("size");
            }
        }

        #endregion
    }
}